/**
 * 
 */
package de.mmis.core.base;

import java.lang.annotation.Annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.TypeVariable;
import java.util.Arrays;
import java.util.Hashtable;

import de.mmis.core.base.Annotations.NoProperty;
import de.mmis.core.base.gclass.GClass;

/**
 * class representing method description objects providing information about a
 * specific method of an ObjectDescriptor
 * 
 * @author Sebastian Bader
 * @author Christian Koch
 */
public class MethodDescriptor implements Comparable<MethodDescriptor> {

	@Retention(RetentionPolicy.RUNTIME)
	@Target(ElementType.PARAMETER)
	public static @interface P {
		/** the name of the parameter */
		String value();

		/** Description of the parameter */
		String help() default "";
	}

	@Retention(RetentionPolicy.RUNTIME)
	@Target(ElementType.METHOD)
	public static @interface methodHelp {
		String helpText();
	}

	private ObjectDescriptor objectDescriptor;
	private Method method;

	private String methodGetterName; // name of getter method (excluding "is" or
										// "get"), or null, if method is no
										// getter
	private String methodHelp;
	private String[] argumentName;
	private String[] argumentHelp;

	/**
	 * creates new method descriptor
	 * 
	 * @param objectDescriptor
	 *            object descriptor owning this method. may be <code>null</code>
	 *            if method is static.
	 * @param method
	 *            method to provide information about
	 */
	public MethodDescriptor(ObjectDescriptor objectDescriptor, Method method) {
		super();
		this.objectDescriptor = objectDescriptor;
		this.method = method;

		// initialize method help text
		Annotation ma = method.getAnnotation(methodHelp.class);
		if (ma == null)
			this.methodHelp = null;
		else
			this.methodHelp = ((methodHelp) ma).helpText();

		// initialize parameter names and help text
		Annotation[][] paraAnnotations = method.getParameterAnnotations();
		String[] paraName = new String[paraAnnotations.length];
		String[] paraHelp = new String[paraAnnotations.length];
		for (int i = 0; i < paraAnnotations.length; i++) {
			paraName[i] = null;
			paraHelp[i] = null;
			for (int j = 0; j < paraAnnotations[i].length; j++) {
				if (paraAnnotations[i][j] instanceof P) {
					P np = (P) paraAnnotations[i][j];
					paraName[i] = np.value();
					paraHelp[i] = np.help();
				}
			}
			if (paraName[i] == null) {
				paraName[i] = "p" + i;
			}
		}
		this.argumentName = paraName;
		this.argumentHelp = paraHelp;

		// test whether method is a getter
		this.methodGetterName = null;

		if (method.getAnnotation(NoProperty.class) == null
				&& !method.getName().equals("getClass")
				&& method.getGenericParameterTypes().length == 0) {
			if (method.getName().startsWith("get")
					&& method.getName().length() > 3
					&& Character.isUpperCase(method.getName().charAt(3)))
				this.methodGetterName = method.getName().substring(3);
			else if (method.getName().startsWith("is")
					&& method.getName().length() > 2
					&& Character.isUpperCase(method.getName().charAt(2)))
				this.methodGetterName = method.getName().substring(2);
		}
	}

	/**
	 * invoke underlying method
	 * 
	 * @param arguments
	 *            the arguments used for the method call
	 * @return the result of dispatching the method represented by this object
	 *         on its object with given parameters
	 * @throws IllegalArgumentException
	 *             if the method is an instance method and the specified object
	 *             argument is not an instance of the class or interface
	 *             declaring the underlying method (or of a subclass or
	 *             implementor thereof); if the number of actual and formal
	 *             parameters differ; if an unwrapping conversion for primitive
	 *             arguments fails; or if, after possible unwrapping, a
	 *             parameter value cannot be converted to the corresponding
	 *             formal parameter type by a method invocation conversion.
	 * @throws IllegalAccessException
	 *             if this Method object enforces Java language access control
	 *             and the underlying method is inaccessible.
	 * @throws InvocationTargetException
	 *             if the underlying method throws an exception.
	 */
	public Object invoke(Object... arguments) throws IllegalArgumentException,
			IllegalAccessException, InvocationTargetException {
		return this.method.invoke(this.objectDescriptor == null ? null
				: this.objectDescriptor.getObject(), arguments);
	}

	/**
	 * @return Number of parameters of this method
	 */
	public int getArgumentCount() {
		return this.method.getParameterTypes().length;
	}

	public String toString(boolean printHelpTexts) {
		StringBuilder sb = new StringBuilder();
		if (printHelpTexts && this.methodHelp != null)
			sb.append(this.methodHelp + "\n");

		sb.append("(");
		sb.append(this.method.getName());
		// sb.append(objectDescriptor.typeToShortString(method.getGenericReturnType())
		// + " "+method.getName() + "(");
		for (int i = 0; i < this.argumentName.length; i++) {
			sb.append(" ");
			sb.append((this.argumentName[i] != null) ? this.argumentName[i]
					: "?");
			sb.append(":" + getParameterType(i).toString());
		}
		sb.append("):");
		sb.append(getReturnType().toString());
		if (printHelpTexts)
			for (int i = 0; i < this.argumentName.length; i++)
				if (this.argumentHelp[i] != null)
					sb.append("\n  " + this.argumentName[i] + " - "
							+ this.argumentHelp[i]);
		return sb.toString();
	}

	@Override
	public String toString() {
		return toString(false);
	}

	/**
	 * @return object descriptor owning this mehtod
	 */
	public ObjectDescriptor getObjectDescriptor() {
		return this.objectDescriptor;
	}

	/**
	 * @return encapsulated method
	 */
	public Method getMethod() {
		return this.method;
	}

	/**
	 * @return name of this method
	 */
	public String getName() {
		return this.method.getName();
	}

	/**
	 * @return true, if this method is a (parameterless) getter (a method
	 *         starting with "is" or "get")
	 */
	public boolean isGetter() {
		return this.methodGetterName != null;
	}

	/**
	 * @return method name excluding leading "is" or "get", or null, if this
	 *         method is no getter
	 */
	public String getGetterName() {
		return this.methodGetterName;
	}

	/**
	 * @param index
	 *            index of argument in parameter list
	 * @return name of parameter on position given by index
	 */
	public String getArgumentName(int index) {
		return this.argumentName[index];
	}

	/**
	 * @param index
	 *            index of argument in parameter list
	 * @return help text of parameter on position given by index, or null, if no
	 *         help text specified
	 */
	public String getArgumentHelp(int index) {
		return this.argumentHelp[index];
	}

	/**
	 * @param index
	 *            index of argument in parameter list
	 * @return type of parameter on position given by index
	 */
	public GClass getParameterType(int index) {
		if (this.objectDescriptor == null)
			return GClass.fromType(
					this.method.getGenericParameterTypes()[index],
					new Hashtable<TypeVariable<?>, GClass>());
		return this.objectDescriptor.typeToClass(this.method
				.getGenericParameterTypes()[index]);
	}

	/**
	 * @return type of return value of this method
	 */
	public GClass getReturnType() {
		if (this.objectDescriptor == null)
			return GClass.fromType(this.method.getGenericReturnType(),
					new Hashtable<TypeVariable<?>, GClass>());
		return this.objectDescriptor.typeToClass(this.method
				.getGenericReturnType());
	}

	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + Arrays.hashCode(this.argumentName);
		result = prime * result + Arrays.hashCode(this.argumentHelp);
		result = prime * result
				+ ((this.method == null) ? 0 : this.method.hashCode());
		result = prime
				* result
				+ ((this.objectDescriptor == null) ? 0 : this.objectDescriptor
						.hashCode());
		return result;
	}

	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		MethodDescriptor other = (MethodDescriptor) obj;
		if (!Arrays.equals(this.argumentName, other.argumentName))
			return false;
		if (!Arrays.equals(this.argumentHelp, other.argumentHelp))
			return false;
		if (this.method == null) {
			if (other.method != null)
				return false;
		} else if (!this.method.equals(other.method))
			return false;
		if (this.objectDescriptor == null) {
			if (other.objectDescriptor != null)
				return false;
		} else if (!this.objectDescriptor.equals(other.objectDescriptor))
			return false;
		return true;
	}

	@Override
	public int compareTo(MethodDescriptor arg0) {
		return getMethod().getName().compareTo(arg0.getMethod().getName());
	}
}